<!-- Licensed under a BSD license. See license.html for license -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>WebGL2 - Visualize Camera with Frustum</title>
<link type="text/css" href="resources/webgl-tutorials.css" rel="stylesheet" />
</head>
<body>
<div class="description">
Visualize Camera with Frustum
</div>
  <canvas id="canvas"></canvas>
  <div id="uiContainer">
    <div id="ui">
    </div>
  </div>
</body>
<!--
This sample uses TWGL (Tiny WebGL) to hide the clutter.
Otherwise the sample would be full of code not related to the point of the sample.
For more info see https://webgl2fundamentals.org/webgl/lessons/webgl-less-code-more-fun.html
-->
<script src="resources/webgl-lessons-ui.js"></script>
<script src="resources/twgl-full.min.js"></script>
<script src="resources/m4.js"></script>
<script>
'use strict';

const vs = `#version 300 es
in vec4 a_position;
in vec4 a_color;

uniform mat4 u_matrix;

out vec4 v_color;

void main() {
  // Multiply the position by the matrix.
  gl_Position = u_matrix * a_position;

  // Pass the vertex color to the fragment shader.
  v_color = a_color;
}
`;

const fs = `#version 300 es
precision highp float;

// Passed in from the vertex shader.
in vec4 v_color;

out vec4 outColor;

void main() {
  outColor = v_color;
}
`;

const colorVS = `#version 300 es
in vec4 a_position;

uniform mat4 u_matrix;

void main() {
  // Multiply the position by the matrix.
  gl_Position = u_matrix * a_position;
}
`;

const colorFS = `#version 300 es
precision highp float;

uniform vec4 u_color;

out vec4 outColor;

void main() {
  outColor = u_color;
}
`;


function main() {
  // Get A WebGL context
  /** @type {HTMLCanvasElement} */
  const canvas = document.querySelector('#canvas');
  const gl = canvas.getContext('webgl2');
  if (!gl) {
    return;
  }

  // setup GLSL programs
  // compiles shaders, links program, looks up locations
  const vertexColorProgramInfo = twgl.createProgramInfo(gl, [vs, fs]);
  const solidColorProgramInfo = twgl.createProgramInfo(gl, [colorVS, colorFS]);

  // Tell the twgl to match position with a_position,
  // normal with a_normal etc..
  twgl.setAttributePrefix("a_");

  // create buffers and fill with data for a 3D 'F'
  const fBufferInfo = twgl.primitives.create3DFBufferInfo(gl);
  const fVAO = twgl.createVAOFromBufferInfo(gl, vertexColorProgramInfo, fBufferInfo);

  function createClipspaceCubeBufferInfo(gl) {
    // first let's add a cube. It goes from 1 to 3
    // because cameras look down -Z so we want
    // the camera to start at Z = 0. We'll put a
    // a cone in front of this cube opening
    // toward -Z
    const positions = [
      -1, -1, -1,  // cube vertices
       1, -1, -1,
      -1,  1, -1,
       1,  1, -1,
      -1, -1,  1,
       1, -1,  1,
      -1,  1,  1,
       1,  1,  1,
    ];
    const indices = [
      0, 1, 1, 3, 3, 2, 2, 0, // cube indices
      4, 5, 5, 7, 7, 6, 6, 4,
      0, 4, 1, 5, 3, 7, 2, 6,
    ];
    return twgl.createBufferInfoFromArrays(gl, {
      position: positions,
      indices,
    });
  }

  // create geometry for a camera
  function createCameraBufferInfo(gl, scale = 1) {
    // first let's add a cube. It goes from 1 to 3
    // because cameras look down -Z so we want
    // the camera to start at Z = 0.
    // We'll put a cone in front of this cube opening
    // toward -Z
    const positions = [
      -1, -1,  1,  // cube vertices
       1, -1,  1,
      -1,  1,  1,
       1,  1,  1,
      -1, -1,  3,
       1, -1,  3,
      -1,  1,  3,
       1,  1,  3,
       0,  0,  1,  // cone tip
    ];
    const indices = [
      0, 1, 1, 3, 3, 2, 2, 0, // cube indices
      4, 5, 5, 7, 7, 6, 6, 4,
      0, 4, 1, 5, 3, 7, 2, 6,
    ];
    // add cone segments
    const numSegments = 6;
    const coneBaseIndex = positions.length / 3;
    const coneTipIndex =  coneBaseIndex - 1;
    for (let i = 0; i < numSegments; ++i) {
      const u = i / numSegments;
      const angle = u * Math.PI * 2;
      const x = Math.cos(angle);
      const y = Math.sin(angle);
      positions.push(x, y, 0);
      // line from tip to edge
      indices.push(coneTipIndex, coneBaseIndex + i);
      // line from point on edge to next point on edge
      indices.push(coneBaseIndex + i, coneBaseIndex + (i + 1) % numSegments);
    }
    positions.forEach((v, ndx) => {
      positions[ndx] *= scale;
    });
    return twgl.createBufferInfoFromArrays(gl, {
      position: positions,
      indices,
    });
  }

  const cameraScale = 20;
  const cameraBufferInfo = createCameraBufferInfo(gl, cameraScale);
  const cameraVAO = twgl.createVAOFromBufferInfo(
      gl, solidColorProgramInfo, cameraBufferInfo);

  const clipspaceCubeBufferInfo = createClipspaceCubeBufferInfo(gl);
  const clipspaceCubeVAO = twgl.createVAOFromBufferInfo(
      gl, solidColorProgramInfo, clipspaceCubeBufferInfo);

  function degToRad(d) {
    return d * Math.PI / 180;
  }

  const settings = {
    rotation: 150,  // in degrees
    cam1FieldOfView: 60,  // in degrees
    cam1PosX: 0,
    cam1PosY: 0,
    cam1PosZ: -200,
    cam1Near: 30,
    cam1Far: 500,
  };
  webglLessonsUI.setupUI(document.querySelector('#ui'), settings, [
    { type: 'slider',   key: 'rotation',        min: 0, max: 360, change: render, precision: 2, step: 0.001, },
    { type: 'slider',   key: 'cam1FieldOfView', min: 1, max: 170, change: render, },
    { type: 'slider',   key: 'cam1PosX',     min: -200, max: 200, change: render, },
    { type: 'slider',   key: 'cam1PosY',     min: -200, max: 200, change: render, },
    { type: 'slider',   key: 'cam1PosZ',     min: -200, max: 200, change: render, },
    { type: 'slider',   key: 'cam1Near',     min: 1, max: 500, change: render, },
    { type: 'slider',   key: 'cam1Far',      min: 1, max: 500, change: render, },
  ]);

  function drawScene(projectionMatrix, cameraMatrix, worldMatrix) {
    // Clear the canvas AND the depth buffer.
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // Make a view matrix from the camera matrix.
    const viewMatrix = m4.inverse(cameraMatrix);

    let mat = m4.multiply(projectionMatrix, viewMatrix);
    mat = m4.multiply(mat, worldMatrix);

    gl.useProgram(vertexColorProgramInfo.program);

    // ------ Draw the F --------

    // Setup all the needed attributes.
    gl.bindVertexArray(fVAO);

    // Set the uniforms
    twgl.setUniforms(vertexColorProgramInfo, {
      u_matrix: mat,
    });

    // calls gl.drawArrays or gl.drawElements
    twgl.drawBufferInfo(gl, fBufferInfo);
  }

  function render() {
    twgl.resizeCanvasToDisplaySize(gl.canvas);

    gl.enable(gl.CULL_FACE);
    gl.enable(gl.DEPTH_TEST);
    gl.enable(gl.SCISSOR_TEST);

    // we're going to split the view in 2
    const effectiveWidth = gl.canvas.clientWidth / 2;
    const aspect = effectiveWidth / gl.canvas.clientHeight;
    const near = 1;
    const far = 2000;

    // Compute a perspective projection matrix
    const perspectiveProjectionMatrix =
        m4.perspective(degToRad(settings.cam1FieldOfView),
        aspect,
        settings.cam1Near,
        settings.cam1Far);

    // Compute the camera's matrix using look at.
    const cameraPosition = [
        settings.cam1PosX,
        settings.cam1PosY,
        settings.cam1PosZ,
    ];
    const target = [0, 0, 0];
    const up = [0, 1, 0];
    const cameraMatrix = m4.lookAt(cameraPosition, target, up);

    let worldMatrix = m4.yRotation(degToRad(settings.rotation));
    worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation));
    // center the 'F' around its origin
    worldMatrix = m4.translate(worldMatrix, -35, -75, -5);

    const {width, height} = gl.canvas;
    const leftWidth = width / 2 | 0;

    // draw on the left with orthographic camera
    gl.viewport(0, 0, leftWidth, height);
    gl.scissor(0, 0, leftWidth, height);
    gl.clearColor(1, 0.8, 0.8, 1);

    drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix);

    // draw on right with perspective camera
    const rightWidth = width - leftWidth;
    gl.viewport(leftWidth, 0, rightWidth, height);
    gl.scissor(leftWidth, 0, rightWidth, height);
    gl.clearColor(0.8, 0.8, 1, 1);

    const perspectiveProjectionMatrix2 =
        m4.perspective(degToRad(60), aspect, near, far);

    // Compute the camera's matrix using look at.
    const cameraPosition2 = [-600, 400, -400];
    const target2 = [0, 0, 0];
    const cameraMatrix2 = m4.lookAt(cameraPosition2, target2, up);

    drawScene(perspectiveProjectionMatrix2, cameraMatrix2, worldMatrix);

    // draw object to represent first camera
    {
      // Make a view matrix from the camera matrix.
      const viewMatrix = m4.inverse(cameraMatrix2);

      let mat = m4.multiply(perspectiveProjectionMatrix2, viewMatrix);
      // use the first's camera's matrix as the matrix to position
      // the camera's representative in the scene
      mat = m4.multiply(mat, cameraMatrix);

      gl.useProgram(solidColorProgramInfo.program);

      // ------ Draw the Camera Representation --------

      // Setup all the needed attributes.
      gl.bindVertexArray(cameraVAO);

      // Set the uniforms
      twgl.setUniforms(solidColorProgramInfo, {
        u_matrix: mat,
        u_color: [0, 0, 0, 1],
      });

      // calls gl.drawArrays or gl.drawElements
      twgl.drawBufferInfo(gl, cameraBufferInfo, gl.LINES);

      // ----- Draw the frustum -------

      mat = m4.multiply(mat, m4.inverse(perspectiveProjectionMatrix));

      // Setup all the needed attributes.
      gl.bindVertexArray(clipspaceCubeVAO);

      // Set the uniforms
      twgl.setUniforms(solidColorProgramInfo, {
        u_matrix: mat,
        u_color: [0, 0, 0, 1],
      });

      // calls gl.drawArrays or gl.drawElements
      twgl.drawBufferInfo(gl, clipspaceCubeBufferInfo, gl.LINES);
    }
  }
  render();
}

main();
</script>
</html>



